// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using SnapObjects.Commons;

namespace SnapObjects.Data.Sqlite
{
    public class SqliteTypeMapper : TypeMapper
    {

        private static readonly HashSet<string> _spatialiteTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "GEOMETRY",
            "GEOMETRYCOLLECTION",
            "LINESTRING",
            "MULTILINESTRING",
            "MULTIPOINT",
            "MULTIPOLYGON",
            "POINT",
            "POLYGON"
        };

        private const string IntegerTypeName = "INTEGER";
        private const string RealTypeName = "REAL";
        private const string BlobTypeName = "BLOB";
        private const string TextTypeName = "TEXT";

        private static readonly SqliteLongTypeMapping _integer = new SqliteLongTypeMapping(IntegerTypeName);
        private static readonly SqliteDoubleTypeMapping _real = new SqliteDoubleTypeMapping(RealTypeName);
        private static readonly SqliteByteArrayTypeMapping _blob = new SqliteByteArrayTypeMapping(BlobTypeName);
        private static readonly SqliteStringTypeMapping _text = new SqliteStringTypeMapping(TextTypeName);

        private static readonly SqliteDateTimeTypeMapping _datetime
            = new SqliteDateTimeTypeMapping(TextTypeName);

        private static readonly SqliteDateTimeOffsetTypeMapping _datetimeoffset
            = new SqliteDateTimeOffsetTypeMapping(TextTypeName);

        private static readonly SqliteDecimalTypeMapping _decimal
            = new SqliteDecimalTypeMapping(RealTypeName);

        private static readonly SqliteGuidTypeMapping _guid
            = new SqliteGuidTypeMapping(BlobTypeName);

        private static readonly SqliteULongTypeMapping _ulong
            = new SqliteULongTypeMapping(IntegerTypeName);

        private readonly Dictionary<string, TypeMapping> _dbTypeMappings;
        private readonly Dictionary<Type, TypeMapping> _clrTypeMappings;

        public SqliteTypeMapper(IDataContextOptions options) : base(options)
        {
            _dbTypeMappings = new Dictionary<string, TypeMapping>(StringComparer.OrdinalIgnoreCase)
            {
                { IntegerTypeName, _integer },
                { RealTypeName, _real },
                { BlobTypeName, _blob },
                { TextTypeName, _text }
            };

            _clrTypeMappings = new Dictionary<Type, TypeMapping>
            {
                { typeof(string), _text },
                { typeof(byte[]), _blob },
                { typeof(bool), new BoolTypeMapping(IntegerTypeName) },
                { typeof(byte), new ByteTypeMapping(IntegerTypeName) },
                { typeof(char), new CharTypeMapping(TextTypeName) },

                // Workaround Microsoft.Data.Sqlite.SqliteDataReader.GetFieldType() issue
                // when the column's value is null in the first record, 
                // the column's dbtype is always int32.
                { typeof(int), new IntTypeMapping(IntegerTypeName) },

                { typeof(long), _integer },
                { typeof(sbyte), new SByteTypeMapping(IntegerTypeName) },
                { typeof(short), new ShortTypeMapping(IntegerTypeName) },
                { typeof(uint), new UIntTypeMapping(IntegerTypeName) },
                { typeof(ulong), _ulong },
                { typeof(ushort), new UShortTypeMapping(IntegerTypeName) },
                { typeof(DateTime), _datetime },
                { typeof(DateTimeOffset), _datetimeoffset },
                { typeof(TimeSpan), new TimeSpanTypeMapping(TextTypeName) },
                { typeof(decimal), _decimal },
                { typeof(double), _real },
                { typeof(float), new FloatTypeMapping(RealTypeName) },
                { typeof(Guid), _guid },
                { typeof(object), new SqliteObjectTypeMapping("OBJECT") }
            };
        }

        public static bool IsSpatialiteType(string columnType)
        {
            return _spatialiteTypes.Contains(columnType);
        }

        protected override IReadOnlyDictionary<Type, TypeMapping> GetClrTypeMappings()
        {
            return _clrTypeMappings;
        }

        protected override IReadOnlyDictionary<string, TypeMapping> GetDbTypeMappings()
        {
            return _dbTypeMappings;
        }

        public override TypeMapping FindMapping(string clrType)
        {
            Check.NotNull(clrType, nameof(clrType));

            if (clrType.Length == 0)
            {
                // This may seem odd, but it's okay because we are matching SQLite's loose typing.
                return _text;
            }

            foreach (var rules in _typeRules)
            {
                var mapping = rules(clrType);
                if (mapping != null)
                {
                    return mapping;
                }
            }

            return _text;
        }

        private readonly Func<string, TypeMapping>[] _typeRules =
        {
            name => Contains(name, "INT") || Contains(name, "BOOL")
            ? _integer 
            : null,
            name => Contains(name, "CHAR") || Contains(name, "CLOB") || Contains(name, "TEXT")
            ? _text
            : null,
            name => Contains(name, "BLOB") 
            ? _blob 
            : null, 
            name => Contains(name, "REAL") || Contains(name, "FLOA") || Contains(name, "DOUB") || Contains(name, "NUMBER")
            ? _real
            : null
        };

        private static bool Contains(string haystack, string needle)
        {
            return haystack.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0;
        }
    }
}
